﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Microsoft.Isam.Esent.Serialization
{
	/// <summary>This class implements writing to a TAR file.</summary>
	/// <remarks>Since ESENT provides no way to do on-line restore, this library can't restore backups.
	/// Use your favorite file manager software to restore a backup. I recommend Total Commander for that.</remarks>
	internal class TapeArchiver
	{
		readonly Stream dest = null;

		const int cbBlock = 512;
		static readonly byte[] s_EmptyBlock;

		static TapeArchiver()
		{
			s_EmptyBlock = new byte[ cbBlock ];
		}

		public TapeArchiver( Stream s )
		{
			dest = s;
		}

		public interface iFileWriter: IDisposable
		{
			/// <summary>Write a portion of data to the TAR.</summary>
			/// <param name="buffer"></param>
			/// <param name="cb"></param>
			void Write( byte[] buffer, int cb );
		}

		internal class HeaderBlock
		{
			static System.DateTime _unixEpoch = new System.DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc );

			static int DateTime2TimeT( System.DateTime datetime )
			{
				System.TimeSpan delta = datetime - _unixEpoch;
				return (System.Int32)( delta.TotalSeconds );
			}

			byte[] buff;

			public HeaderBlock( string strName, ulong ulSize )
			{
				buff = new byte[ 512 ];

				// Magic + version
				PrintAt( 0x101, "ustar 00" );

				// File name
				SetName( strName );

				// File mode (whatever it means)
				PrintAt( 100, "000644 " );

				// Size
				PrintNumericAt( 124, 12, (long)ulSize, false );

				// Timestamp
				int iTime = DateTime2TimeT( DateTime.UtcNow );
				PrintNumericAt( 136, 12, iTime, false );

				// Type of file
				PrintAt( 156, "0" );

				// The checksum

				// First set the checksum to all ASCII spaces
				PrintAt( 148, "".PadLeft( 8, ' ' ) );

				// Sum all bytes
				int sum = buff
					.Select( b => (int)b )
					.Sum();

				// Set the checksum
				PrintNumericAt( 148, 8, sum, true );
			}

			void ReplaceAt( int ind, IEnumerable<byte> newData )
			{
				foreach( byte v in newData )
				{
					buff[ ind ] = v;
					ind++;
				}
			}

			void PrintAt( int ind, string txt )
			{
				var src = System.Text.Encoding.ASCII.GetBytes( txt );
				ReplaceAt( ind, src );
			}

			void PrintNumericAt( int ind, int len, long val, bool bNullTerm )
			{
				string octValue = Convert.ToString( val, 8 ).PadLeft( len, '0' );

				if( bNullTerm )
					octValue += '\0';

				if( octValue.Length > len )
					octValue = octValue.Substring( octValue.Length - len );
				PrintAt( ind, octValue );
			}

			void SetName( string name )
			{
				var bbName = Encoding.ASCII.GetBytes( name );
				if( bbName.Length > 255 )
					throw new ArgumentOutOfRangeException( "name", "The name is too long for the TAR format." );

				if( bbName.Length < 100 )
					ReplaceAt( 0, bbName );
				else
				{
					ReplaceAt( 0, bbName.Skip( bbName.Length - 100 ) );
					ReplaceAt( 345, bbName.Take( bbName.Length - 100 ) );
				}
			}

			public void Write( Stream dest )
			{
				dest.Write( buff, 0, 512 );
			}
		}

		internal class FileWriter: iFileWriter
		{
			readonly Stream dest = null;
			readonly string fileName = null;

			readonly ulong ulSize = 0;
			ulong ulWritten = 0;

			public FileWriter( Stream s, String _fileName, ulong fileSize )
			{
				dest = s;
				ulSize = fileSize;
				this.fileName = _fileName;

				HeaderBlock hb = new HeaderBlock( _fileName, fileSize );
				hb.Write( dest );
			}

			void iFileWriter.Write( byte[] buffer, int cb )
			{
				if( cb < 0 )
					throw new ArgumentOutOfRangeException( "cb", "TapeArchiver.iFileWriter.Write: the argument is negative." );

				ulong ulAfterwards = ulWritten + (ulong)cb;
				if( ulAfterwards > ulSize )
					throw new EndOfStreamException( "TapeArchiver.iFileWriter.Write is trying to write beyond the end of the " + fileName + " file." );

				dest.Write( buffer, 0, cb );
				ulWritten = ulAfterwards;
			}

			void IDisposable.Dispose()
			{
				// Check everything has been written.
				if( ulWritten != ulSize )
					throw new EndOfStreamException( "TapeArchiver.iFileWriter is disposed while the file " + fileName + " is still incomplete." );

				// Write the remainder.
				int remainder = (int)( ulWritten % cbBlock );

				// if( 0 == remainder )
				// 	remainder = cbBlock;
				if( 0 != remainder )
					dest.Write( s_EmptyBlock, 0, remainder );

				dest.Flush();
			}
		}

		/// <summary>Write the file header, and prepare for writing the data.</summary>
		/// <param name="fileName"></param>
		/// <param name="fileSize"></param>
		/// <returns></returns>
		public iFileWriter AddFile( String fileName, ulong fileSize )
		{
			return new FileWriter( dest, fileName, fileSize );
		}

		/// <summary>Write an empty block, marking the end of the archive.</summary>
		public void CloseArchive()
		{
			dest.Write( s_EmptyBlock, 0, cbBlock );
			dest.Flush();
		}
	}
}
